﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using BizMock.Adapter.Runtime;
using Microsoft.BizTalk.Message.Interop;
using System.Xml;
using System.IO;
using System.Xml.Schema;
using System.Text.RegularExpressions;

namespace BizMock
{
    public delegate void BizMockMessageValidationAction(IBaseMessage msg);

    public delegate void XmlContentsValidationAction(XmlDocument xmlData);

    public class BizMockMessage
    {
        public BizMockMessage( string file ): this( new string[]{file} )
        { 
        }

        private string[] _files;
        public BizMockMessage(params string[] files)
        {
            _files = files;
            foreach (string file in _files)
            {
                string newFileName = string.Format("{0}.{1}", file, Guid.NewGuid().ToString());
                System.IO.File.Copy(file, newFileName);
            }
            Properties = new List<ContextProperty>();

            
        }

        public void Promote(string name, string ns, object value)
        {
            Properties.Add(new ContextProperty(name, ns, value, true));
        }

        public void Write(string name, string ns, object value)
        {
            Properties.Add(new ContextProperty(name, ns, value, false));
        }

        public void Remove(string name, string ns)
        {
            ContextProperty prop = Properties.Find(x => x.Name == name && x.Namespace == ns);
            if (prop != null)
                Properties.Remove(prop);
        }

        public string[] Files { get { return _files; } }

        public List<ContextProperty> Properties { get; set; }
        public void FindAndReplace(int partIndex, Dictionary<string, string> findReplaceDict)
        {
            foreach (string find in findReplaceDict.Keys)
            {
                FindAndReplace(partIndex, find, findReplaceDict[find]);
            }
        }

        public void FindAndReplace(int partIndex, string findText, string replaceText)
        {
            if (partIndex >= Files.Length)
                throw new BizMockException(string.Format("out of bounds part, message has {0} parts", Files.Length));
            string file = Files[partIndex];
            string tempFile = string.Format("temp{0}", file);

            using (Stream fs = new FileStream(file, FileMode.Open))
            {
                using (StreamWriter sw = new StreamWriter(tempFile))
                {
                    using (StreamReader sr = new StreamReader(fs))
                    {
                        while (!sr.EndOfStream)
                        {
                            string line = sr.ReadLine();
                            if (line.IndexOf(findText) != -1)
                            {
                                line = line.Replace(findText, replaceText);
                            }
                            sw.WriteLine(line);
                        }
                    }
                }
            }
            using (Stream fs = new FileStream(tempFile, FileMode.Open))
            {
                using (StreamWriter sw = new StreamWriter(file))
                {
                    using (StreamReader sr = new StreamReader(fs))
                    {
                        while (!sr.EndOfStream)
                        {
                            string line = sr.ReadLine();
                            sw.WriteLine(line);
                        }
                    }
                }
            }
            System.IO.File.Delete(tempFile);
        }

        public void FindXmlNodeAndReplace(int partIndex, Dictionary<string, string> xpathValueDict)
        {
            foreach (string xpath in xpathValueDict.Keys)
            {
                FindXmlNodeAndReplace(partIndex, xpath, xpathValueDict[xpath]);
            }
        }

        public void FindXmlNodeListAndReplace(int partIndex, Dictionary<string, string[]> xpathValueDict)
        {
            foreach (string xpath in xpathValueDict.Keys)
            {
                FindXmlNodeListAndReplace(partIndex, xpath, xpathValueDict[xpath]);
            }
        }

        public void FindXmlNodeAndReplace(int partIndex, string xpath, string innerTextValue)
        {
            if (partIndex >= Files.Length)
                throw new BizMockException(string.Format("out of bounds part, message has {0} parts", Files.Length));
            string file = Files[partIndex];
            string tempFile = string.Format("temp{0}", file);

            XmlDocument xdoc = new XmlDocument();
            xdoc.Load(file);
            XmlNode xNode = xdoc.SelectSingleNode(xpath);
            if (xNode == null)
                throw new Exception(string.Format("xpath query {0} does not match schema of file {1}", xpath, file));

            xdoc.SelectSingleNode(xpath).InnerText = innerTextValue;
            xdoc.Save(tempFile);
            xdoc.Load(tempFile);
            xdoc.Save(file);
            System.IO.File.Delete(tempFile);
        }

        public void FindXmlNodeListAndReplace(int partIndex, string xpath, params string[] innerTextValue)
        {
            if (partIndex >= Files.Length)
                throw new BizMockException(string.Format("out of bounds part, message has {0} parts", Files.Length));
            string file = Files[partIndex];
            string tempFile = string.Format("temp{0}", file);

            XmlDocument xdoc = new XmlDocument();
            xdoc.Load(file);
            XmlNodeList xNodeList = xdoc.SelectNodes(xpath);
            if (xNodeList == null)
                throw new Exception(string.Format("xpath query {0} does not match schema of file {1}", xpath, file));
            int i = 0;
            foreach (XmlNode xmlNode in xNodeList)
            {
                xmlNode.InnerText = innerTextValue[i++];
            }
            xdoc.Save(tempFile);
            xdoc.Load(tempFile);
            xdoc.Save(file);
            System.IO.File.Delete(tempFile);
        }
    }

    public class XmlMessageVerifier : FlatMessageVerifier
    {
        private XmlSchema _schema = null;
        public XmlSchema Schema
        {
            get { return _schema; }
            set
            {
                _schema = value;
                this.CustomValidation(validateSchema);
            }
        }

        private string _rootName = null;
        public string RootName
        {
            get { return _rootName; }
            set { _rootName = value; }
        }

        private void validateSchema(XmlDocument xdoc)
        {
            if (_schema == null)
                return;
            if (string.IsNullOrEmpty(xdoc.DocumentElement.NamespaceURI) && string.IsNullOrEmpty(_schema.TargetNamespace))
            {
                System.Diagnostics.Debug.WriteLine("BizMock: TargetNamespace empty, both xml and schema");
            }
            else
            {
                if (xdoc.DocumentElement.NamespaceURI != _schema.TargetNamespace)
                {
                    throw new BizMockException(string.Format("document namespace {0}, does not match with expected {1}", xdoc.DocumentElement.NamespaceURI, _schema.TargetNamespace));
                }
            }
            if (_rootName != null && xdoc.DocumentElement.LocalName != _rootName)
            {
                throw new BizMockException(string.Format("document root {0}, does not match with expected {1}", xdoc.DocumentElement.Name, _rootName));
            }
            List<string> errorList = new List<string>();
            XmlReaderSettings settings = new XmlReaderSettings();
            settings.Schemas.Add(_schema);
            settings.ValidationType = ValidationType.Schema;
            settings.ValidationEventHandler +=
                new ValidationEventHandler(delegate(object sender, ValidationEventArgs e)
                {
                    if (e.Severity == XmlSeverityType.Error)
                    {
                        errorList.Add(e.Message);
                    }
                });
            MemoryStream ms = new MemoryStream();
            xdoc.Save(ms);
            ms.Position = 0;
            XmlReader reader = XmlReader.Create(ms, settings);
            while (reader.Read()) { };
            if (errorList.Count > 0)
            {
                string errorMessage = "schema validation errors:";
                foreach (string error in errorList)
                {
                    errorMessage += error + ";";
                }
                throw new BizMockException(errorMessage);
            }
        }

        private Dictionary<string, int> childNodeCountAsserts = new Dictionary<string, int>();
        public void ExpectedChildCount(string xpath, int count)
        {
            childNodeCountAsserts.Add(xpath, count);
            this.CustomValidation(validateNodeChildCount);
        }

        private void validateNodeChildCount(XmlDocument xdoc)
        {
            foreach (string xpath in nodeCountAsserts.Keys)
            {
                XmlNodeList xnodeList = xdoc.SelectNodes(xpath);
                int expectedCount = childNodeCountAsserts[xpath];
                int childcount = 0;
                foreach (XmlNode xnode in xnodeList)
                {
                    childcount += xnode.ChildNodes.Count;
                }
                if (expectedCount != childcount)
                    throw new BizMockException(string.Format(" expected {0} nodes for {1}", expectedCount, xpath));
            }
        }

        private Dictionary<string, int> nodeCountAsserts = new Dictionary<string, int>();
        public void ExpectedCount(string xpath, int count)
        {
            nodeCountAsserts.Add(xpath, count);
            this.CustomValidation(validateNodeCount);
        }

        private void validateNodeCount(XmlDocument xdoc)
        {
            foreach (string xpath in nodeCountAsserts.Keys)
            {
                XmlNodeList xnodeList = xdoc.SelectNodes(xpath);
                int expectedCount = nodeCountAsserts[xpath];
                if (expectedCount != xnodeList.Count)
                    throw new BizMockException(string.Format(" expected {0} nodes for {1}", expectedCount, xpath));
            }
        }

        public void ExpectedValue(string xpath, string value)
        {
            ExpectedValues(xpath, true, false, new string[] { value });
        }

        public void ExpectedValue(string xpath, bool exact, string value)
        {
            ExpectedValues(xpath, exact, false, new string[] { value });
        }

        public void ExpectedInnerXml(string xpath, string value)
        {
            ExpectedValues(xpath, true, true, new string[] { value });
        }

        public void ExpectedInnerXml(string xpath, bool exact, string value)
        {
            ExpectedValues(xpath, exact, true, new string[] { value });
        }

        public void ExpectedValueList(string xpath, params string[] values)
        {
            ExpectedValues(xpath, true, false, values);
        }

        public void ExpectedValueList(string xpath, bool exact, params string[] values)
        {
            ExpectedValues(xpath, exact, false, values);
        }

        public void ExpectedInnerXmlList(string xpath, params string[] values)
        {
            ExpectedValues(xpath, true, true, values);
        }

        public void ExpectedInnerXmlList(string xpath, bool exact, params string[] values)
        {
            ExpectedValues(xpath, exact, true, values);
        }

        private Dictionary<string, string[]> innerNodeValueAsserts = new Dictionary<string, string[]>();
        public void ExpectedValues(string xpath, bool exact, bool innerXml, params string[] values)
        {
            if (innerNodeValueAsserts.ContainsKey(xpath))
            {
                innerNodeValueAsserts[xpath] = values;
            }
            else
            {
                innerNodeValueAsserts.Add(xpath, values);
            }
            if (exact && innerXml)
                this.CustomValidation(validateExactInnerXml);
            else if (!exact && innerXml)
                this.CustomValidation(validateContainsInnerXml);
            else if (exact && !innerXml)
                this.CustomValidation(validateExactInnerText);
            else if (!exact && !innerXml)
                this.CustomValidation(validateContainsInnerText);
        }

        private void validateContainsInnerXml(XmlDocument xdoc)
        {
            validateInnerNode(xdoc, false, true);
        }

        private void validateExactInnerXml(XmlDocument xdoc)
        {
            validateInnerNode(xdoc, true, true);
        }

        private void validateContainsInnerText(XmlDocument xdoc)
        {
            validateInnerNode(xdoc, false, false);
        }

        private void validateExactInnerText(XmlDocument xdoc)
        {
            validateInnerNode(xdoc, true, false);
        }

        private void validateInnerNode(XmlDocument xdoc, bool exactMatch, bool innerXml)
        {
            foreach (string xpath in innerNodeValueAsserts.Keys)
            {
                bool isList = innerNodeValueAsserts[xpath].Length > 1 ? true : false;
                XmlNodeList xnodeList = xdoc.SelectNodes(xpath);
                if (xnodeList.Count == 0)
                    throw new BizMockException(string.Format("xpath query not found {0}", xpath));
                string[] values = new string[xnodeList.Count];
                if (isList)
                {
                    int i = 0;
                    foreach (XmlNode xnode in xnodeList)
                    {
                        validateInnerNode(xnode, xpath, i++, exactMatch, innerXml);
                    }
                }
                else
                {
                    XmlNode xnode = xnodeList[0];
                    validateInnerNode(xnode, xpath, 0, exactMatch, innerXml);
                }
            }
        }

        private void validateInnerNode(XmlNode xnode, string xpath, int index, bool exactMatch, bool innerXml)
        {
            string value = innerXml ? xnode.InnerXml : xnode.InnerText;
            if (exactMatch && value != innerNodeValueAsserts[xpath][index])
                throw new BizMockException(string.Format(" expected {0} at {1}", innerNodeValueAsserts[xpath][index], xpath));
            else if (!value.Contains(innerNodeValueAsserts[xpath][index]))
                throw new BizMockException(string.Format(" expected inner xml contains {0} at {1}", innerNodeValueAsserts[xpath][index], xpath));
        }
    }

    public class FlatMessageVerifier : MessageVerifierBase
    {
        internal class BizMockMessageContextInfo
        {
            public BizMockMessageContextInfo(string propName, string propNamespace, bool expectedPromoted, string expectedValue, bool exact)
            {
                PropName = propName;
                PropNamespace = propNamespace;
                Value = expectedValue;
                Exact = exact;
                IsPromoted = expectedPromoted;
            }

            private string _propName;

            public string PropName
            {
                get
                {
                    return _propName;
                }
                set
                {
                    _propName = value;
                }
            }

            private string _propNamespace;

            public string PropNamespace
            {
                get
                {
                    return _propNamespace;
                }
                set
                {
                    _propNamespace = value;
                }
            }

            private string _value;

            public string Value
            {
                get { return _value; }
                set { _value = value; }
            }
            private bool _exact;

            public bool Exact
            {
                get { return _exact; }
                set { _exact = value; }
            }
            private bool _isPromoted;

            public bool IsPromoted
            {
                get { return _isPromoted; }
                set { _isPromoted = value; }
            }
        }

        IDictionary<string, BizMockMessageContextInfo> _contextAsserts = new Dictionary<string, BizMockMessageContextInfo>();
        internal IDictionary<string, BizMockMessageContextInfo> ContextAsserts
        {
            get { return _contextAsserts; }
            set { _contextAsserts = value; }
        }

        List<string> _regularExpressions = new List<string>();
        public void Match(string regExp)
        {
            _regularExpressions.Add(regExp);
            this.CustomValidation(verifyContentMatchRegularExpression);
        }

        private void verifyContentMatchRegularExpression(IBaseMessage message)
        {
            StreamReader sr = new StreamReader(message.BodyPart.Data);
            string bodyPartContent = sr.ReadToEnd();
            message.BodyPart.Data.Position = 0;

            foreach (string regExp in _regularExpressions)
            {
                if (regExp != null)
                {
                    System.Text.RegularExpressions.Match match = Regex.Match(bodyPartContent, regExp);
                    if (!match.Success)
                        throw new BizMockException(string.Format("Regular expression {0} does not match content {1}", regExp, bodyPartContent));
                }
            }
        }

        public void ExpectedContextProperty(string propName, string propNamespace, bool expectPromoted, string expectedValue)
        {
            ExpectedContextProperty(propName, propNamespace, expectPromoted, expectedValue, true);
        }

        public void ExpectedContextProperty(string propName, string propNamespace, bool expectPromoted, string expectedValue, bool exact)
        {
            string key = string.Format("{0}@{1}", propName, propNamespace);
            BizMockMessageContextInfo expectationArtifact =
                new BizMockMessageContextInfo(propName, propNamespace, expectPromoted, expectedValue, exact);

            if (_contextAsserts.ContainsKey(key))
            {
                _contextAsserts[key] = expectationArtifact;
            }
            else
            {
                _contextAsserts.Add(key, expectationArtifact);
            }
        }

        public override void Reset()
        {
            _regularExpressions.Clear();
            _contextAsserts.Clear();
        }
    }

    public interface IMultiPartVerifier
    {
        MessageVerifierBase[] Verifiers{get;}
    }

    public class MessageVerifierBase
    {
        private IList<BizMockMessageValidationAction> _validationDelegates = new List<BizMockMessageValidationAction>();

        public IList<BizMockMessageValidationAction> ValidationDelegates
        {
            get { return _validationDelegates; }
            set { _validationDelegates = value; }
        }

        public void CustomValidation(XmlContentsValidationAction validation)
        {
            CustomValidation(
                delegate(IBaseMessage msg)
                {
                    msg.BodyPart.Data.Position = 0;
                    XmlDocument xmlDoc = new XmlDocument();
                    xmlDoc.Load(msg.BodyPart.Data);
                    msg.BodyPart.Data.Position = 0;

                    validation(xmlDoc);
                }
            );
        }

        /// <summary>
        ///     Validate the message using a method that should throw an Exception if the message is invalid.
        ///     (For example a delegate that calls NUnit/MSTest Assertions on the message)
        /// </summary>
        public void CustomValidation(BizMockMessageValidationAction validation)
        {
            if (!_validationDelegates.Contains(validation))
                _validationDelegates.Add(validation);
        }

        public virtual void Reset()
        {
            _validationDelegates.Clear();
        }
    }

    internal class BizMockMessageFactory : IBaseMessageFactory
    {
        public IBaseMessage CreateMessage()
        {
            return new BizMockBaseMessage();
        }

        public IBaseMessageContext CreateMessageContext()
        {
            return new BizMockMessageContext();
        }

        public IBaseMessagePart CreateMessagePart()
        {
            return new BizMockMessagePart();
        }

        public IBasePropertyBag CreatePropertyBag()
        {
            return new BizMockPropertyBag();
        }
    }

    internal class BizMockBaseMessage : IBaseMessage
    {
        private IList<string> orderedPartNames = new List<string>();
        private IDictionary<string, IBaseMessagePart> parts = new Dictionary<string, IBaseMessagePart>();
        private string bodyPartName;
        private Exception errorInfo;
        private Guid messageId = Guid.NewGuid();
        private IBaseMessageContext messageContext;

        public void GetSize(out ulong lSize, out bool fImplemented)
        {
            lSize = 0L;
            fImplemented = false;
        }

        public IBaseMessagePart GetPartByIndex(int index, out string partName)
        {
            partName = orderedPartNames[index];
            return parts[partName];
        }

        public void AddPart(string partName, IBaseMessagePart part, bool bBody)
        {
            this.orderedPartNames.Add(partName);
            this.parts[partName] = part;
            if (bBody)
            {
                this.bodyPartName = partName;
            }
        }

        public IBaseMessagePart GetPart(string partName)
        {
            return parts[partName];
        }

        public void RemovePart(string partName)
        {
            this.orderedPartNames.Remove(partName);
            this.parts.Remove(partName);
        }

        public Exception GetErrorInfo()
        {
            return this.errorInfo;
        }

        public void SetErrorInfo(Exception errInfo)
        {
            this.errorInfo = errInfo;
        }

        public Guid MessageID
        {
            get { return this.messageId; }
        }

        public IBaseMessageContext Context
        {
            get
            {
                if (this.messageContext == null)
                {
                    this.messageContext = new BizMockMessageContext();
                }
                return this.messageContext;
            }
            set { this.messageContext = value; }
        }

        public IBaseMessagePart BodyPart
        {
            get
            {
                return this.parts[this.bodyPartName];
            }
        }

        public string BodyPartName
        {
            get { return this.bodyPartName; }
        }

        public int PartCount
        {
            get { return this.orderedPartNames.Count; }
        }

        public bool IsMutable
        {
            get { return true; }
        }
    }

    internal class BizMockMessagePart : IBaseMessagePart
    {
        private Stream originalDataStream;
        private Guid partId = Guid.NewGuid();
        private IBasePropertyBag partProperties = new BizMockPropertyBag();

        public void GetSize(out ulong lSize, out bool fImplemented)
        {
            lSize = 0L;
            fImplemented = false;
        }

        public Stream GetOriginalDataStream()
        {
            return this.originalDataStream;
        }

        public Guid PartID
        {
            get { return this.partId; }
        }

        public IBasePropertyBag PartProperties
        {
            get { return this.partProperties; }
            set { this.partProperties = value; }
        }

        public string ContentType
        {
            get
            {
                return (string)this.PartProperties.Read("ContentType", "");
            }
            set
            {
                this.PartProperties.Write("ContentType", "", value);
            }
        }

        public string Charset
        {
            get
            {
                return (string)this.PartProperties.Read("Charset", "");
            }
            set
            {
                this.PartProperties.Write("Charset", "", value);
            }
        }

        public Stream Data
        {
            get { return this.originalDataStream; }
            set { this.originalDataStream = value; }
        }

        public bool IsMutable
        {
            get { return true; }
        }
    }

    internal class BizMockPropertyBag : IBasePropertyBag
    {
        private IList<PropertyKey> orderedProperties = new List<PropertyKey>();
        private IDictionary<PropertyKey, object> properties = new Dictionary<PropertyKey, object>();

        public object ReadAt(int index, out string strName, out string strNamespace)
        {
            PropertyKey key = orderedProperties[index];
            strName = key.Name;
            strNamespace = key.Namespace;

            return properties[key];
        }

        public object Read(string strName, string strNamespace)
        {
            PropertyKey key = new PropertyKey(strName, strNamespace);
            return properties[key];
        }

        public void Write(string strName, string strNamespace, object obj)
        {
            PropertyKey key = new PropertyKey(strName, strNamespace);
            properties[key] = obj;
        }

        public uint CountProperties
        {
            get { return (uint)orderedProperties.Count; }
        }

        internal sealed class PropertyKey
        {
            public string Name { get; set; }
            public string Namespace { get; set; }

            public PropertyKey(string propName, string propNamespace)
            {
                this.Name = propName;
                this.Namespace = propNamespace;
            }

            public bool Equals(PropertyKey other)
            {
                if (ReferenceEquals(null, other)) return false;
                if (ReferenceEquals(this, other)) return true;
                return Equals(other.Name, Name) && Equals(other.Namespace, Namespace);
            }

            public override bool Equals(object obj)
            {
                if (ReferenceEquals(null, obj)) return false;
                if (ReferenceEquals(this, obj)) return true;
                if (obj.GetType() != typeof(PropertyKey)) return false;
                return Equals((PropertyKey)obj);
            }

            public override int GetHashCode()
            {
                unchecked
                {
                    return ((Name != null ? Name.GetHashCode() : 0) * 397) ^ (Namespace != null ? Namespace.GetHashCode() : 0);
                }
            }
        }
    }

    internal class BizMockMessageContext : BizMockPropertyBag, IBaseMessageContext
    {
        private HashSet<PropertyKey> promotedProperties = new HashSet<PropertyKey>();

        public void Promote(string strName, string strNamespace, object obj)
        {
            promotedProperties.Add(new PropertyKey(strName, strNamespace));
            base.Write(strName, strNamespace, obj);
        }

        public void AddPredicate(string strName, string strNameSpace, object obj)
        {
        }

        public bool IsPromoted(string strName, string strNamespace)
        {
            return promotedProperties.Contains(new PropertyKey(strName, strNamespace));
        }

        public ContextPropertyType GetPropertyType(string strName, string strNameSpace)
        {
            return IsPromoted(strName, strNameSpace) ?
                ContextPropertyType.PropPromoted :
                ContextPropertyType.PropWritten;
        }
    }



}
